﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Net;
using System.Diagnostics;
using System.IO;
using System.Xml.XPath;
using System.Xml;
using System.Threading;
using System.Security.AccessControl;
using System.Security.Authentication;
using System.ComponentModel;

namespace WinSCP.API
{
    public class Client
    {
        private static string BINARY_PATH = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Resources/WinSCP.com");
        private static string LOG_PATH = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Log.xml");
        private Queue<string> stdOut = new Queue<string>();

        #region [ Constructors ]

        public Client() { }

        public Client(string ipAddress, int port, string username, string password)
            : this(ipAddress, port, username, password, null, null) { }

        public Client(string ipAddress, int port, string username, string password, string binaryPath)
            : this(ipAddress, port, username, password, binaryPath, null) { }

        public Client(string ipAddress, int port, string username, string password, string binaryPath, string logPath)
            : this(IPAddress.Parse(ipAddress), port, username, password, binaryPath, logPath) { }

        public Client(IPAddress ipAddress, int port, string username, string password)
            : this(ipAddress, port, username, password, null, null) { }

        public Client(IPAddress ipAddress, int port, string username, string password, string binaryPath)
            : this(ipAddress, port, username, password, binaryPath, null) { }

        public Client(IPAddress ipAddress, int port, string username, string password, string binaryPath, string logPath)
            : this(new IPEndPoint(ipAddress, port), username, password, binaryPath, logPath) { }

        public Client(IPEndPoint endPoint, string username, string password, string binaryPath, string logPath)
        {
            this._targetAddress = endPoint.Address;
            this._portNumber = endPoint.Port;
            this._username = username;
            this._password = password;

            this._logPath = String.IsNullOrWhiteSpace(logPath) ?
                LOG_PATH : logPath;

            this._binaryPath = String.IsNullOrWhiteSpace(binaryPath) ?
                BINARY_PATH : binaryPath;
        }

        internal void CommandLine_OutputDataReceived(object sender, DataReceivedEventArgs e)
        {
            if (!String.IsNullOrWhiteSpace(e.Data)) 
            {
                //lock (stdOut)
                //    stdOut.Enqueue(e.Data
                //        .Replace((char)10, (char)0)
                //        .Replace((char)13, (char)0));
                
                lock (stdOut)
                    stdOut.Enqueue(e.Data);
            }
        }

        #endregion

        /// <summary>
        /// Connects to the remote host. In order to connect error-free, I do not use the batch or confirm options
        /// with the CLI. Using these options causes RSA identification to automatically fail. Instead I am attempting
        /// a connection, storing the fingerprint (if the connection is RSA-enabled) and then reconnecting to the
        /// server using the stored fingerprint. The drawback to this workaround is that the first time you try to
        /// connect to a server you could potentially be connecting twice.
        /// </summary>
        public void Connect()
        {
            lock (stdOut)
                stdOut.Clear();

            string lineData = String.Empty;

        AutoAcceptKey:
            if (String.IsNullOrWhiteSpace(this._rsaFingerprint))
            {
                // Indicates that this is the first attempt to connect to the remote host.

                if (!IsRunning)
                    this._isRunning = CommandLine.Start();

                CommandLine.BeginOutputReadLine();
                CommandLine.StandardInput.WriteLine("option batch on");
                CommandLine.StandardInput.WriteLine("option confirm off");
                CommandLine.StandardInput.WriteLine("open {0}:{1}@{2}:{3}",
                    this._username, this._password, this._targetAddress.ToString(), this._portNumber);
            }
            else
            {
                // Indicates that a previous attempt resulted in a failure because of the
                // RSA key mismatch so we want to connect with the stored fingerprint.

                if (IsRunning)
                    Disconnect();
                    
                this._isRunning = CommandLine.Start();

                CommandLine.BeginOutputReadLine();
                CommandLine.StandardInput.WriteLine("option batch on");
                CommandLine.StandardInput.WriteLine("option confirm off");
                CommandLine.StandardInput.WriteLine("open {0}:{1}@{2}:{3} -hostkey=\"{4}\"",
                    this._username, this._password, this._targetAddress.ToString(), this._portNumber, this._rsaFingerprint);
            }

            // Gives the CLI a chance to establish the connection before we starting tugging on the queue.
            Thread.Sleep(1000);

            // This little ditty right here allows us to automatically accept any host key. I hate
            // that I have to do this but WinSCP doesn't have a built-in option to do it for me
            // without already knowing the host key.
            while (true)
            {
                if (stdOut.Count > 0)
                {
                    lock (stdOut)
                        lineData = stdOut.Dequeue();
                }

                if (lineData.StartsWith("ssh-rsa") && String.IsNullOrWhiteSpace(this._rsaFingerprint))
                {
                    this._rsaFingerprint = lineData;
            
                    lock(stdOut)
                        stdOut.Clear();

                    goto AutoAcceptKey;
                }

                if (lineData.StartsWith("Active session"))
                    break;

                if (lineData.Contains("Access denied"))
                {
                    Disconnect();
                    throw new AuthenticationException("Login failed to the remote host.");
                }
            }

            lock(stdOut)
                stdOut.Clear();

            this._isConnected = true;
        }

        /// <summary>
        /// Sends the exit command to the CLI and terminates the process without
        /// disposing of the object. This allows us to reuse the process object.
        /// </summary>
        public void Disconnect()
        {
            if (IsRunning)
            {
                do
                {
                    CommandLine.StandardInput.WriteLine("exit");
                }
                while (!CommandLine.WaitForExit(500));
            }

            this._commandLine = null;
            this._isRunning = false;
            this._isConnected = false;
        }

        /// <summary>
        /// Gets the active <see cref="LogReader"/> for the log path.
        /// </summary>
        public LogReader Log
        {
            get
            {
                if (this._log == null)
                    this._log = new LogReader(this._logPath);

                return this._log;
            }
        }
        private LogReader _log;

        #region [ Commands ]

        /// <summary>
        /// Gets the current working folder.
        /// </summary>
        /// <remarks>
        /// This method assumes that the command is either the first or
        /// middle in a series of commands. The reason for this is
        /// because there is no point in making a connect simply to
        /// get the current folder and disconnect.
        /// </remarks>
        public string WorkingFolder
        {
            get
            {
                if (!IsRunning || !IsConnected)
                    Connect();

                return SendCommand("pwd", false)
                    .Split((char)10, (char)13).Last();
            }
        }

        /// <summary>
        /// Changes the current folder.
        /// </summary>
        /// <param name="path">The path to navigate to.</param>
        /// <remarks>
        /// This method assumes that the command is either the first or
        /// middle in a series of commands. The reason for this is
        /// because there is no point in making a connect simply to
        /// change folders and disconnect.
        /// </remarks>
        public void ChangeDirectory(string path)
        {
            if (!IsRunning || !IsConnected)
                Connect();

            SendCommand("cd " + path, false);
        }

        /// <summary>
        /// Sends the 'ls' command to the CLI resulting in a list of the folder contents being dumped
        /// to the XML log file. Once the command has completed the log file is parsed for the results.
        /// </summary>
        /// <returns>
        /// A list of <see cref="FileSystemObject"/> entities representing the contents of the PWD.
        /// </returns>
        public List<FileSystemObject> GetContentsList()
        {
            return GetContentsList(false);
        }

        /// <summary>
        /// Sends the 'ls' command to the CLI for the PWD resulting in a list of the folder contents
        /// being dumping to the XML log file. Once the command has completed the log file is parse
        /// for the results.
        /// </summary>
        /// <param name="includeFolderSizes">
        /// True to recursively get the contents of the first set of children in the root in order to
        /// determine the size of the contents inside a directory.
        /// </param>
        /// <returns>
        /// A list of <see cref="FileSystemObject"/> entities representing the contents of the PWD.
        /// </returns>
        public List<FileSystemObject> GetContentsList(bool includeFolderSizes)
        {
            string workingFolder = WorkingFolder;

            if (UpdateProgress != null)
                UpdateProgress(Worker, new ProgressChangedEventArgs(0, "Retrieving folder contents list..."));

            SendCommand("ls", !includeFolderSizes);

            if (UpdateProgress != null)
                UpdateProgress(Worker, new ProgressChangedEventArgs(10, "Waiting for response from server..."));

            do
            {
                Thread.Sleep(100);
                Log.Update();
            }
            while (Log.Listings == null ||
                   Log.Listings.Where(l => l.Destination == workingFolder).Count() < 1);

            List<FileSystemObject> listing = Log.Listings
                .Where(l => l.Destination == workingFolder).Last().Contents;

            if (includeFolderSizes)
            {
                if (UpdateProgress != null)
                    UpdateProgress(Worker, new ProgressChangedEventArgs(30, "Retrieving subfolder sizes..."));

                stdOut.Clear();
                SendCommand("call du {0}", workingFolder);

                // Looping like this will block until the CLI is finished processing the commands
                // by determining when it has finished sending data to the standard output stream.
                do
                {
                    if (UpdateProgress != null)
                        UpdateProgress(Worker, new ProgressChangedEventArgs(50, "Waiting for command to complete..."));

                    if (stdOut.Count > 0)
                    {
                        lock (stdOut)
                            stdOut.Dequeue();
                    }
                }
                while (stdOut.Count > 0);

                if (UpdateProgress != null)
                    UpdateProgress(Worker, new ProgressChangedEventArgs(70, "Waiting for log file to close..."));

                while (!Log.IsClosed)
                    Thread.Sleep(100);

                Log.Update();

                CallCommand duCall = Log.Calls
                    .Where(c => c.WorkingFolder == workingFolder).Last();

                if (UpdateProgress != null)
                    UpdateProgress(Worker, new ProgressChangedEventArgs(80, "Writing folder sizes to the datasource..."));

                Dictionary<string, long> folderSizes = Log.GetDirectoryUsage(duCall.Output);
                foreach (FileSystemObject dir in listing)
                {
                    if (dir.ObjType == FileSystemObject.ObjectType.File)
                        continue;

                    if (folderSizes.ContainsKey(workingFolder + "/" + dir.Filename))
                        listing[listing.IndexOf(dir)].Bytes = folderSizes[workingFolder + "/" + dir.Filename];
                }
            }

            if (UpdateProgress != null)
                UpdateProgress(Worker, new ProgressChangedEventArgs(100, "Process complete, returning control to caller..."));

            return listing;
        }

        /// <summary>
        /// Sends the 'ps' command to the custom caller of the CLI and gets a list of all processes
        /// that are running under the 'ventrilo' executable.
        /// </summary>
        /// <returns>
        /// A list of running port numbers on the machine.
        /// </returns>
        public List<int> GetRunningPorts()
        {
            if (UpdateProgress != null)
                UpdateProgress(Worker, new ProgressChangedEventArgs(0, "Retrieving process list from server..."));

            stdOut.Clear();
            SendCommand("call ps -ef | grep ventrilo");

            if (UpdateProgress != null)
                UpdateProgress(Worker, new ProgressChangedEventArgs(50, "Waiting for log file to close..."));

            while (!Log.IsClosed)
                Thread.Sleep(100);

            stdOut.Clear();
            Log.Update();

            CallCommand psCall = Log.Calls
                .Where(c => c.Command == "ps -ef | grep ventrilo").Last();

            if (UpdateProgress != null)
                UpdateProgress(Worker, new ProgressChangedEventArgs(100, "Writing process list to the datasource..."));

            return Log.GetProcessList(psCall.Output);
        }

        public void CompressPayload(string source, string target)
        {
            if (UpdateProgress != null)
                UpdateProgress(Worker, new ProgressChangedEventArgs(-1, "Compressing contents of " + source + "..."));

            if (!IsRunning || !IsConnected)
                Connect();

            stdOut.Clear();
            SendCommand("call tar -czf {0} {1}", false, target, source);

            if (UpdateProgress != null)
                UpdateProgress(Worker, new ProgressChangedEventArgs(-1, "Contents compressed to " + target + "."));
        }


        public void TransferPayload(string remoteSource, string localTarget)
        {
            if (UpdateProgress != null)
                UpdateProgress(Worker, new ProgressChangedEventArgs(-1, 
                    "Receiving file " + remoteSource + " to " + localTarget + "..."));

            if (!IsRunning || !IsConnected)
                Connect();

            stdOut.Clear();
            SendCommand("get {0} {1}", false, remoteSource, localTarget);

            if (UpdateProgress != null)
                UpdateProgress(Worker, new ProgressChangedEventArgs(-1, 
                    "Received file " + remoteSource + " to " + localTarget + "."));
        }

        #endregion

        public event ProgressChangedEventHandler UpdateProgress;

        #region [ Command Processing ]

        public string SendCommand(string command, bool manageConnection, params string[] args)
        {
            return SendCommand(String.Format(command, args), manageConnection);
        }

        public string SendCommand(string command, params string[] args)
        {
            return SendCommand(String.Format(command, args));
        }

        public string SendCommand(string command)
        {
            return SendCommand(command, true);
        }

        public string SendCommand(string command, bool manageConnection)
        {
            // Determine whether or not we should open and close the CLI.
            if (manageConnection && (!IsRunning || !IsConnected))
                Connect();

            // Clear the output buffer.
            lock(stdOut)
                stdOut.Clear();

            // Send the command to the CLI.
            CommandLine.StandardInput.WriteLine(command);

            // Half a second seems like a reasonable amount of time to wait for a response...
            Thread.Sleep(500); 

            StringBuilder response = new StringBuilder();
            while (true)
            {
                if (stdOut.Count > 0)
                {
                    lock (stdOut)
                        response.AppendLine(stdOut.Dequeue());
                }
                else
                    break;
            }

            // Determine whether or not we should open and close the CLI.
            if (manageConnection && (IsConnected || IsRunning))
                Disconnect();

            return response.ToString()
                .TrimEnd((char)10, (char)13);
        }

        #endregion

        #region [ Configuration Properties ]

        internal Process CommandLine
        {
            get 
            {
                if (this._commandLine == null)
                {
                    this._commandLine = new Process();
                    CommandLine.StartInfo.FileName = this._binaryPath;
                    CommandLine.StartInfo.Arguments = "/log=" + this._logPath;
                    CommandLine.StartInfo.UseShellExecute = false;
                    CommandLine.StartInfo.RedirectStandardInput = true;
                    CommandLine.StartInfo.RedirectStandardOutput = true;
                    CommandLine.StartInfo.CreateNoWindow = true;
                    CommandLine.OutputDataReceived += new DataReceivedEventHandler(CommandLine_OutputDataReceived);
                }

                return this._commandLine; 
            }
        }
        private Process _commandLine;

        public IPAddress TargetAddress
        {
            get { return this._targetAddress; }
        }
        private IPAddress _targetAddress;

        public int PortNumber
        {
            get { return this._portNumber; }
        }
        private int _portNumber;

        public string Username
        {
            get { return this._username; }
        }
        private string _username;

        public string Password
        {
            get { return this._password; }
        }
        private string _password;

        public string LogPath
        {
            get { return this._logPath; }
        }
        private string _logPath;

        public string BinaryPath
        {
            get { return this._binaryPath; }
        }
        private string _binaryPath;

        public object StateObj
        {
            get { return this._stateObj; }
            set { this._stateObj = value; }
        }
        private object _stateObj;

        #endregion

        #region [ Status/Session Properties ]

        public bool IsRunning
        {
            get { return this._isRunning; }
        }
        private bool _isRunning;

        public bool IsConnected
        {
            get { return this._isConnected; }
        }
        private bool _isConnected;

        public string RsaFingerprint
        {
            get { return this._rsaFingerprint; }
        }
        private string _rsaFingerprint;

        public BackgroundWorker Worker
        {
            get { return this._worker; }
            set { this._worker = value; }
        }
        private BackgroundWorker _worker;

        #endregion
    }
}